<?php

/**
 * FacebookFilter handles Facebook's authentication and stores permissions in session.
 *
 * @author     NC
 *
 * The session is refreshed every time we receive a valid signed request:
 *   - Initial access to the tabbed profile (session kept by Facebook)
 *   - Ajax request on the tabbed profile (session kept by Facebook)
 *   - Initial access to the canvas iframe (session kept by the user's browser)
 *
 * The informations stored in session are:
 *   - uid: the user's Facebook uid
 *   - page_ids: the IDs of pages and applications the user is an admin of
 *
 * The page_ids are used to determine wether the user should be granted specific permissions:
 *   - page_admin: the user is ad admin of the current page
 *   - super_admin: the user is an admin of the FSmartShop appication
 *
 * Since page_ids are fetched by an FQL request, in order to prevent unnecessary calls
 * to Facebook's API, they are only fetched when a page requires the page_admin or
 * super_admin permission and the cached list does not enable us to grant the
 * permission.
 *
 * Hence, we MUST NOT use the presence of these permissions nor the contents of the
 * page_ids session attribute to determine wether an user is an admin. Permissions
 * are only set correctly on pages requiring them.
 *
 * A notable exception is the initial display of the tabbed profile, where the uid
 * is unknown and the page_admin permission is granted based on the fb_sig_is_admin
 * parameter.
 *
 */
class FacebookFilter extends sfFilter
{
    protected $has_loaded_fql = false;
    protected $has_fql_error = false;


    protected function preExecute()
    {
        $sf_user = $this->getContext()->getUser();
        $request = $this->getContext()->getRequest();

        try {
            $this->updateSfUser($sf_user, $request);
        } catch (Exception $e) {
            $this->getContext()->getLogger()->debug("{FacebookFilter} updateSfUser error: ".$e->getMessage());
            $sf_user->logOut();
        }

        if ($sf_user->isAuthenticated()) {
            $this->updateFbUser($sf_user);
            static::updatePermissions($sf_user);

            // Update page_admin relations when required data was fetched from FB
            if ($this->has_loaded_fql && !$this->has_fql_error)
                $this->updatePageAdmins($sf_user);
        }
    }


    private function updateSfUser($sf_user, $request)
    {
        if ($signed_request = App::getFacebookClient()->getSignedRequest()) {
            vtnsLog::info("{FacebookFilter} using signed_request: %s", $signed_request);

            if (isset($signed_request['page']['id'])) {
                // We're in a new profile iframe
                $page_id = $signed_request['page']['id'];
                vtnsLog::debug("{FacebookFilter} New page profile, page_id: $page_id");
                $sf_user->setPageId($page_id);
            } else {
                // We're either in an old profile or an out-of tab iframe
                if (isset($signed_request['profile_id'])) {
                    $page_id = $signed_request['profile_id'];
                    vtnsLog::debug("{FacebookFilter} Old page profile, page_id: $page_id");
                    $sf_user->setPageId($page_id);
                    $sf_user->logOut();
                }
            }

            if (isset($signed_request['app_data'])) {
                $request->setParameter('app_data', $signed_request['app_data']);
            }

            if (isset($signed_request['page']['admin'])) {
                $sf_user->setIsAdmin(!empty($signed_request['page']['admin']));
            }

        } else {
            if (isset($request['page_id'])) {
                $sf_user->setPageId($request['page_id']);
            } else {
                vtnsLog::debug("{FacebookFilter} No page id could be found in request");
            }
        }

        if ($uid = App::getFacebookClient()->getUser()) {
            vtnsLog::debug("{FacebookFilter} Logged-in user: $uid");
            $sf_user->logIn(App::getFacebookClient()->getUser());
        } else {
            vtnsLog::debug("{FacebookFilter} No logged-in user");
            $sf_user->logOut();
        }
    }


    /**
     * Updates information for an authenticated user
     */
    private function updateFbUser($sf_user)
    {
        // Get or create user
        $fb_user = Doctrine_Core::getTable('User')->findOneByUid($sf_user->getUid());
        if (!$fb_user) {
            $fb_user = new User();
            $fb_user->uid = $sf_user->getUid();
            $fb_user->updateInfo();
        }

        // User is now authenticated
        $fb_user->is_auth = true;

        // Extra parameters, no API call required
        $signed_request = App::getFacebookClient()->getSignedRequest();
        if (!empty($signed_request['user']['locale']))
            $fb_user->locale = $signed_request['user']['locale'];

        // Save() must be performed before updatePageAdmins() because of foreign keys
        $fb_user->save();
        $sf_user->setFbUser($fb_user);
    }


    /**
     * Creates or updates the page_admin relations for the current user
     */
    private function updatePageAdmins($sf_user)
    {
        $uid = $sf_user->getUid();
        $page_ids = $sf_user->getAttribute('page_ids');
        $con = Doctrine_Manager::getInstance()->connection();

        // Insert new page_admins
        if (!empty($page_ids) && is_array($page_ids)) {
            $page_list = implode(',', $page_ids);
            $in_pages = ' WHERE page_id IN ('.implode(',', $page_ids).')';
            $not_in_pages = ' AND page_id NOT IN ('.implode(',', $page_ids).')';

            $sql = 'INSERT INTO page_admin (page_id, uid, is_admin, created_at, updated_at)';
            $sql .= ' (SELECT page_id, '.$uid.', 1, NOW(), NOW() FROM page '.$in_pages.')';
            $sql .= ' ON DUPLICATE KEY UPDATE is_admin = 1, updated_at = NOW();';
            $con->execute($sql);
        } else {
            $not_in_pages = '';
        }

        // Remove invalid page_admins
        $sql = 'UPDATE page_admin SET is_admin = 0, updated_at = NOW() WHERE uid = '.$uid.$not_in_pages;
        $con->execute($sql);
    }


    protected function updatePermissions($sf_user)
    {
        $fb_user = $sf_user->getFbUser();

        if ($this->actionRequiresCredential('page_admin')) {
            $this->getContext()->getLogger()->debug("{FacebookFilter} Action requires page_admin permission");
            // Allow page admin and super admins
            if (($sf_user->getPageId() && $this->isPageAdmin($sf_user, $sf_user->getPageId())) || $this->isSuperAdmin($sf_user)) 
                $sf_user->addCredential('page_admin');
            // Do not remove page_admin credentials when no page_id is present, prevents sesion cookie regeneration
            elseif($sf_user->getPageId())
                $sf_user->removeCredential('page_admin');
        }

        if ($this->actionRequiresCredential('super_admin')) {
            $this->getContext()->getLogger()->debug("{FacebookFilter} Action requires super_admin permission");
            if($this->isSuperAdmin($sf_user))
                $sf_user->addCredential('super_admin');
            else
                $sf_user->removeCredential('super_admin');
        }

        if ($this->actionRequiresCredential('vtns_sales')) {
            vtnsLog::debug("{FacebookFilter} Action requires super_admin permission");

            if ($fb_user->is_salesrep || $this->isSuperAdmin($sf_user)) {
                $sf_user->addCredential('vtns_sales');
            } else {
                $sf_user->removeCredential('vtns_sales');
            }
        }

        if ($this->actionRequiresCredential('ads_user')) {
            vtnsLog::debug("{FacebookFilter} Action requires ads_user permission");
            try {
                $me = App::getFacebookClient()->api('/me');
                if ($me)
                    $sf_user->addCredential('ads_user');
            } catch (Exception $e) {
                vtnsLog::debug("{FacebokFilter} updatePermissions error: ".$e->getMessage());
                $sf_user->logOut();
            }
        }
    }

    protected function isSuperAdmin($sf_user)
    {
        // SuperAdmin is either hard-coded in DB, either admin of the App on FB
        return ($sf_user->getFbUser() && $sf_user->getFbUser()->is_super_admin) ||
                $this->isPageAdmin($sf_user, App::getMainApiId());
    }


    private function isPageAdmin($sf_user, $page_id)
    {
        $page_ids = $sf_user->getAttribute('page_ids');
        if (is_array($page_ids) && in_array($page_id, $page_ids)) {
            // The user is admin of the page/application
            return true;
        } elseif (!$this->has_loaded_fql) {
            // Reload the user's page list
            $sf_user->setAttribute('page_ids', $this->getPageIds($sf_user->getUid()));
            return $this->isPageAdmin($sf_user, $page_id);
        } else {
            // The user is *not* an admin of the page
            return false;
        }
    }


    private function getPageIds($uid)
    {
        try {
            $page_ids = array();
            $r = App::call_api(array(
                    'method'  => 'fql.multiquery',
                    'queries' => json_encode(array(
                            'perm' => 'SELECT manage_pages FROM permissions WHERE uid ='.$uid,
                            'page_ids' => 'SELECT page_id FROM page_admin WHERE uid ='.$uid,
                            'app_ids'  => 'SELECT application_id FROM developer WHERE developer_id ='.$uid,
                            )),
                    ));
            foreach ($r as $result) {
                if (!array_key_exists('fql_result_set', $result))
                    throw new Exception("Unexpected FQL response: ".var_export($r, true));

                if ($result['name'] === 'perm') {
                    if ($result['fql_result_set'][0]['manage_pages'])
                        continue;
                    else
                        throw new sfException('Missing manage_pages permission');
                }

                if (is_array($result['fql_result_set']))
                    foreach ($result['fql_result_set'] as $page)
                        $page_ids[] = reset($page);
            }
        } catch (Exception $e) {
            // If an error occurs, the page *will* be reloaded because of missing credentials
            $this->getContext()->getLogger()->err("{FacebookFilter} getPageIds($uid): ".$e->getMessage());
            $this->has_fql_error = true;
            $this->getContext()->getUser()->logOut();
        }

        $this->getContext()->getLogger()->debug("{FacebookFilter} pages: ".implode(', ', $page_ids));
        $this->has_loaded_fql = true;
        return $page_ids;
    }


    private function actionRequiresCredential($credential, $values = null)
    {
        if ($values === null)
            $values = $this->getAction()->getCredential();

        if (!is_array($values))
            return $credential === $values;

        foreach ($values as $value)
            if ($this->actionRequiresCredential($credential, $value))
                return true;

        return false;
    }


    private function getAction()
    {
        return $this->getContext()->getActionStack()->getLastEntry()->getActionInstance();
    }
}

